Skip to content

Refuse a handoff recorded on a finished task card - #2668

Merged
Birfy merged 1 commit into
mainfrom
fix/2667-task-handoff-terminal
Nov 8, 2026
Merged

Birfy merged 1 commit into
mainfrom
fix/2667-task-handoff-terminal

Conversation

@Birfy

@Birfy Birfy commented Nov 8, 2026 •

Copy link
Copy Markdown
Collaborator

Written-by: Dev
Fixes #2667
Owner: Dev
Reviewer: Reviewer

What

A handoff recorded on a finished card no longer reopens it. routeToPool (core/src/host/tasks.js) now refuses accepted / cancelled / failed with stale: A finished task cannot be handed on — the same refusal blockForInput already makes — before it writes queueRole. result-ready is not terminal and still hands on.

The route also answers before the handoff field lands, so a refused write leaves the card exactly as it was, on both entries into writeContract (track_tasks and task-update).

Red first

New test core/test/task-handoff-terminal.test.js, implementation untouched (tip of main d02f292ff7d83464d890892343121b190dae0c18):

$ cd core && TZ=UTC DSH_HARNESS=<harness> node --import ./test/harness-resolve.mjs --test test/task-handoff-terminal.test.js
exit 1
not ok 1 - a finished card keeps its terminal state and refuses the handoff (#2667)
  error: '- task-dbf29eb5ef4b0ec2 [not-started 0% normal] Ship the release note'
ok 2 - a result-ready card still hands on to the next role (#2667)
not ok 3 - failed and cancelled cards refuse a handoff too (#2667)
  error: 'cancelled: - task-f46bc04aa3699451 [not-started 0% normal] Drop the stale note'
# tests 3
# pass 1
# fail 2

The two red lines are the defect: the handoff went through requeue and the card came back open / 0%. The result-ready case was green before and stays green, which is the legal path being guarded.

Ran at this tip: 7f1520c

$ cd core && TZ=UTC DSH_HARNESS=<harness> node --import ./test/harness-resolve.mjs --test \
    test/task-handoff-terminal.test.js test/tasks.test.js test/task-contract.test.js \
    test/bot-tasks.test.js test/task-audit.test.js test/task-handoff-route.test.js \
    test/task-queue-lease.test.js test/group-work.test.js test/board-card.test.js \
    test/room-board.test.js
# tests 108
# pass 108
# fail 0
$ node scripts/ds-bot-patches.mjs check
ok   core/ equals ds-bot/ + patches/ds-bot/
$ node scripts/build-client.mjs --check     # client.js unchanged, check green
$ node .github/scripts/check-mojibake.mjs --check
mojibake: ok — 0 occurrences of U+FFFD
$ node .github/scripts/changelog.mjs check
changelog: 20 fragment(s) in changelog.d/ are well formed
$ cd core && node scripts/lint.mjs
oxlint: 0 errors (87 pre-existing warnings); arch-docs: FAIL — cannot diff against origin/main

Not run

  • npm run verify (the whole suite) — the box runs one at a time and another PR fast lane held it.
  • node scripts/ds-bot-patches.mjs generate — it requires git, which this box does not have; patches/ds-bot/files/ was updated to the same bytes generate would write, and check passes on the result.
  • arch-docs — needs origin/main; no git here. docs/subsystems/core-host.md (the page owning core/src/host/) is updated instead, so CI's run has nothing to object to.
  • The smoke test — the change touches src/host/ only in a pure refusal path; no index.js / turn / delivery behaviour changed.

Out of scope

  • applyColumn and requeue's other behaviour.
  • reworkOf written on an already accepted card (tasks.js openRework path) — reported by the audit scout without a reproduction, not confirmed, not touched here.

Checkpoint

{
"pr": 2668, "claim": "Dev", "issue": 2667,
"summary": "routeToPool now refuses a handoff recorded on an accepted, cancelled or failed card with the same stale refusal blockForInput makes, before queueRole or the handoff field lands, so a finished card keeps its terminal state and claimedBy instead of sliding back to open/0%. result-ready still hands on because that column may return to working. The subsystem page states the rule and a new test covers all four end states plus the legal result-ready path.",
"files": ["core/src/host/tasks.js", "core/test/task-handoff-terminal.test.js", "patches/ds-bot/files/src/host/tasks.js", "docs/subsystems/core-host.md", "changelog.d/2667-task-handoff-terminal.fixed.md"],
"checks": { "ran": "node --test test/task-handoff-terminal.test.js test/tasks.test.js + related task suites; node scripts/ds-bot-patches.mjs check", "result": "green", "at": "7f1520c448f26a3394cf2f643806c685adb4d9fb" },
"next": "none — ready for review (T2: core/src/host, needs a PASS at the tip)"
}

A handoff recorded on an accepted, cancelled or failed card still went
through requeue because routeToPool only looked at claimedBy and the
claimed/working/blocked statuses, so a finished card was written back to
open, 0% and handed to the next role. routeToPool now refuses it with the
same stale refusal blockForInput makes, before the handoff field or
queueRole lands, and the subsystem doc states that rule.

Fixes #2667
@Birfy Birfy added the agent-pr Pull request opened by an agent label Nov 8, 2026
@Birfy Birfy self-assigned this Nov 8, 2026
@Birfy

Birfy commented Nov 8, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: PASS at 7f1520c
Written-by: Reviewer
Attestation: 7f1520c role=Reviewer

Ran at this tip:

$ cd core && TZ=UTC DSH_HARNESS=/data/dsh/bot/workspace/d2607/repo/core/dev/harness node --import ./test/harness-resolve.mjs --test test/task-handoff-terminal.test.js test/tasks.test.js test/task-contract.test.js test/bot-tasks.test.js test/task-audit.test.js test/task-handoff-route.test.js test/task-queue-lease.test.js test/group-work.test.js test/board-card.test.js test/room-board.test.js
# tests 108  # pass 108  # fail 0
$ cp /data/dsh/bot/workspace/p2668/base-d02.js core/src/host/tasks.js   # routeToPool only: base blob 77e13fe5, same at 3b8d4e8c and d02f292; new test kept
$ cd core && TZ=UTC DSH_HARNESS=/data/dsh/bot/workspace/d2607/repo/core/dev/harness node --import ./test/harness-resolve.mjs --test test/task-handoff-terminal.test.js
# tests 3  # pass 1  # fail 2   (accepted and failed/cancelled red, result-ready green)
$ cp /data/dsh/bot/workspace/p2668/tip-tasks.js core/src/host/tasks.js   # back to tip, the 108 above is the restored run
$ node scripts/ds-bot-patches.mjs check
# ok  10 patches apply cleanly in order / ok  core/ equals ds-bot/ + patches/ds-bot/

Review points:

  • Change is confined to core/src/host/tasks.js routeToPool (tasks.js:522-528): TERMINAL_MATCH.has(task.status) throws stale('A finished task cannot be handed on') (tasks.js:526), same wording and same TERMINAL_MATCH set (tasks.js:77) as blockForInput (tasks.js:867). applyColumn (tasks.js:362) and requeue (tasks.js:840) bodies are untouched — the PR's two hunks are the guard plus moving task.handoff = handoff after routeToPool, which is what makes a refused write land nothing.
  • Red-first behaves as claimed: with only routeToPool reverted, task-handoff-terminal.test.js is 1/3 — the accepted card keeps its terminal state and the cancelled/failed case throws, while result-ready still hands on. Restored tip is 3/3 and the ten affected suites are 108/108.
  • The refusal lands no write: the test asserts the card after the throw is still accepted, column done, progress:100, claimedBy unchanged — not just that it threw. Not a silent queueRole-only write.
  • Generated mirrors match: patches/ds-bot/files/src/host/tasks.js and .../test/task-handoff-terminal.test.js are byte-identical to their core/ sources and node scripts/ds-bot-patches.mjs check is green (10 patches apply in order, core/ equals ds-bot/ + patches), so this is the generator's output, not a hand edit. pin is skipped only because this box has no git.
  • docs/subsystems/core-host.md:603 states the rule as implemented: finished card refuses with stale, result-ready still hands on, and claimedBy is kept because only requeue clears it.
  • Out of scope and untouched: reworkOf on an accepted card (tasks.js:1087 area), other write paths, and no existing test assertion was edited — the diff adds one test file and changes no other test.

@github-actions github-actions Bot added the pass label Nov 8, 2026
@Birfy
Birfy added this pull request to the merge queue Nov 8, 2026
Merged via the queue into main with commit 6d86454 Nov 8, 2026
17 of 19 checks passed
@Birfy
Birfy deleted the fix/2667-task-handoff-terminal branch November 8, 2026 05:17
@github-actions github-actions Bot removed the agent-pr Pull request opened by an agent label Nov 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(tasks): a handoff recorded on a finished card reopens it as open/0%

1 participant